package jannex;

import jannex.label.LabelSet;
import jannex.mapping.*;

import java.lang.reflect.Field;
import java.util.*;

/**
 * Binds Mapping and ResultSetHandler for a class
 * @param <E>
 */
public class Binder<E> {

    private final JannexBuilder jannexBuilder;
    private final Class<E> elementClass;
    private final MappingFactory mappingFactory;
    private final ResultSetHandlerFactory resultSetHandlerFactory;

    private String table;
    private String schema;
    private ResultSetHandler<E> resultSetHandler;
    private Builder<E> objectBuilder;

    private Map<String, SimpleField> fields;

    private LabelSet orderBy;
    private LabelSet uniqueConstraints;
    private String id;

    private static class SimpleField {

        private final String fieldName;
        private String  label;
        private String  table;
        private String type;
        private int modifiers;

        private List<SimpleField> children; // TODO

        private SimpleField(String fieldName, String label, String table, String type, int modifiers) {
            this.fieldName = fieldName;
            this.label = label;
            this.table = table;
            this.type = type;
            this.modifiers = modifiers;
        }

        public SimpleField(String fieldName) {
            this.fieldName = fieldName;

        }
    }

    public Binder(
            JannexBuilder jannexBuilder,
            MappingFactory mappingFactory,
            ResultSetHandlerFactory resultSetHandlerFactory,
            Class<E> elementClass) {
        this.jannexBuilder = jannexBuilder;
        this.mappingFactory = mappingFactory;
        this.resultSetHandlerFactory = resultSetHandlerFactory;
        this.elementClass = elementClass;
        this.table = elementClass.getName();
    }


    public Binder<E> table(String name) {
        this.table = name;
        return this;
    }

    public Binder<E> schema(String schema) {
        this.schema = schema;
        return this;
    }

    public Binder<E> objectBuilder(Builder<E> objectBuilder) {
        this.objectBuilder = objectBuilder;
        return this;
    }

    public Binder<E> handler(ResultSetHandler<E> resultSetHandler) {
        this.resultSetHandler = resultSetHandler;
        return this;
    }

    private SimpleField getField(String fieldName) {
        String lowerName = fieldName.toLowerCase();
        if (fields == null || !fields.containsKey(lowerName)) {
            field(fieldName);
        }
        return fields.get(lowerName);
    }

    public Binder<E> field(String fieldName, String label, String table, String type, int modifiers) {
        if (fields == null) {
            fields = new HashMap<>();
        }
        fields.put(fieldName.toLowerCase(), new SimpleField(fieldName, label, table, type, modifiers));
        return this;
    }

    public Binder<E> field(String fieldName, String label) {
        return field(fieldName, label, null, null, -1);
    }

    public Binder<E> field(String fieldName) {
        return field(fieldName, null);
    }

    public final Binder<E> fieldsMap(Map<String, String> map) {
        for (Map.Entry<String, String> entryField : map.entrySet()) {
            field(entryField.getKey(), entryField.getValue());
        }
        return this;
    }

    public final Binder<E> fields(String... fieldNames) {
        for (String fieldName : fieldNames) {
            field(fieldName);
        }
        return this;
    }

    public final Binder<E> setFieldLabel(String fieldName, String label) {
        getField(fieldName).label = label;
        return this;
    }

    public final Binder<E> setFieldType(String fieldName, String type) {
        getField(fieldName).type = type;
        return this;
    }

    public final Binder<E> setFieldModifiers(String fieldName, int modifiers) {
        getField(fieldName).modifiers = modifiers;
        return this;
    }

    public final Binder<E> setFieldTable(String fieldName, String table) {
        getField(fieldName).table = table;
        return this;
    }

    public Binder<E> entity() {
        // TODO sub builder
        return null;
    }

    public Binder<E> id(String idLabel) {
        this.id = idLabel;
        return this;
    }

    public Binder<E> uniqueConstraints(String... uniqueConstraints) {
        this.uniqueConstraints = LabelSet.of(uniqueConstraints);
        return this;
    }

    public Binder<E> uniqueConstraints(Iterable<String> uniqueConstraints) {
        this.uniqueConstraints = LabelSet.builder().addAll(uniqueConstraints).build();
        return this;
    }

    public Binder<E> orderBy(String... orderBys) {
        this.orderBy = LabelSet.of(orderBys);
        return this;
    }

    public Binder<E> orderBy(Iterable<String> orderBys) {
        this.orderBy = LabelSet.builder().addAll(orderBys).build();
        return this;
    }

    public Binder<E> orderBy(String orderBy, boolean asc) {
        this.orderBy = LabelSet.of(orderBy + (asc ? "" : " DESC"));
        return this;
    }

    @SuppressWarnings("unchecked")
    public JannexBuilder register() {

        MappedField<E>[] fieldsArray;
        if (fields == null) {
            fieldsArray = mappingFactory.createAll(elementClass);
        } else {
            fieldsArray = mapSimpleFields();
        }

        Mapping<E> mapping = mappingFactory.create(elementClass, fieldsArray, schema);

        if (table == null) {
            table = mapping.getTable();
        }
        if (schema == null) {
            schema = mapping.getSchema();
        }

        if (uniqueConstraints == null) {
            uniqueConstraints = LabelSet.of(mapping.getUniqueConstraints());
        }
        if (orderBy == null) {
            orderBy = LabelSet.of(mapping.getOrderBy());
        }
        if (id == null) {
            id = mapping.getId();
        }


//        for (int i = 0; i < fieldsArray.length; i++) {
//            MappedField<E> field = fieldsArray[i];
//            int modifiers = field.getModifiers();
//            if (field.getFieldName().equalsIgnoreCase(id)) {
//                modifiers |= MappingModifiers.ID;
//            }
//            if (field.getModifiers() != modifiers) {
//                fieldsArray[i] = new MappedField<E>(
//                        field.getField(), field.getSetter(), field.getGetter(), field.getSchema(),
//                        field.getTable(), field.getLabel(), modifiers, field.getType(), field.getFields());
//            }
//        }

        if (resultSetHandler == null) {
            resultSetHandler = resultSetHandlerFactory.create(mapping, objectBuilder);
        }

        jannexBuilder.addHandler(elementClass, resultSetHandler);

        return jannexBuilder.addMapping(elementClass, mapping);
    }

    private MappedField<E>[] mapSimpleFields() {
        @SuppressWarnings("unchecked")
        MappedField<E>[] fieldsArray = (MappedField<E>[]) new MappedField[fields.size()];
        int i = 0;
        for (SimpleField simpleField : fields.values()) {
            Field field;
            try {
                field = elementClass.getDeclaredField(simpleField.fieldName);
            } catch (NoSuchFieldException e) {
                throw new JannexException("Field not found '" + simpleField.fieldName + "' in class "
                        + elementClass.getName() + ".");
            }

            String label = simpleField.label;
            String table = simpleField.table;
            String type = simpleField.type;
            int modifiers = simpleField.modifiers;

            if (table == null && this.table != null) {
                table = this.table;
            }
            if (table == null && this.table != null) {
                table = this.table;
            }

            fieldsArray[i++] = mappingFactory.create(
                    elementClass, field, simpleField.label, simpleField.table, simpleField.type, simpleField.modifiers);
        }
        return fieldsArray;
    }

}
